Capitolo 1: Introduzione¶

1. 0 Dati del dataset¶

Il dataset lo si può trovare al seguente link: https://www.kaggle.com/datasets/shashwatwork/cerebral-stroke-predictionimbalaced-dataset. Di seguito sono presenti le colonne, con il loro relativo tipo di dato, del nostro dataset.

  1. Id
    • Unique identifier
  2. Gender
    • Male
    • Female
    • Other
  3. Age
    • Age of the patient
  4. Hypertension
    • 0 if the patient doesn't have hypertension
    • 1 if the patient has hypertension
  5. Heart_disease
    • 0 if the patient doesn't have any heart diseases
    • 1 if the patient has a heart disease
  6. Ever_married
    • No
    • Yes
  7. Work_type
    • children
    • Govt_jov
    • Never_worked
    • Private
    • Self-employed
  8. Residence_type
    • Rural
    • Urban
  9. Avg_glucose_level
    • Average glucose level in blood
  10. BMI
    • body mass index
  11. Smoking_status
    • formerly smoked
    • never smoked
    • smokes
    • Unknown
  12. stroke
    • 1 if the patient had a stroke
    • 0 if not

1.1 Idea iniziale di analisi da fare¶

Come prime idee, abbiamo avuto le seguenti domande:

  1. Il matrimonio aumenta il rischio di ictus?
  2. Il fumo basta per aumentare il rischio o deve essere combinato ad altri fattori scatenanti?
  3. Quanto influisce l’ambiente esterno?

Dopodiché abbiamo evoluto l'analisi, tralasciando anche un po' le domande iniziali che avevamo. Le analisi che abbiami fatto sono le seguenti:

  1. Matrimonio e correlazione con l'ictus
  2. Fumo e correlazione con l'ictus
  3. BMI, livelli di glucosio correlati con l'ictus
  4. Tipo di lavoro correlato all'ictus

1.2 Importa dati e mostra contenuto tabella¶

Come prima cosa importiamo i dati.

In [1]:
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import plotly.figure_factory as ff

df = pd.read_csv("data/DatasetProgetto.csv")
In [2]:
print("Esempio dei dati contenuti nel csv")
df.head()
Esempio dei dati contenuti nel csv
Out[2]:
id gender age hypertension heart_disease ever_married work_type Residence_type avg_glucose_level bmi smoking_status stroke
0 46136 Male 14.0 0 0 No Never_worked Rural 161.28 19.1 NaN 0
1 16523 Female 8.0 0 0 No Private Urban 110.89 17.6 NaN 0
2 30669 Male 3.0 0 0 No children Rural 95.12 18.0 NaN 0
3 30468 Male 58.0 1 0 Yes Private Urban 87.96 39.2 never smoked 0
4 56543 Female 70.0 0 0 Yes Private Rural 69.04 35.9 formerly smoked 0
In [3]:
print("Informazioni utili:")
df.info()
#si può notare che ci sono 201 valori null per la colonna bmi
Informazioni utili:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 43400 entries, 0 to 43399
Data columns (total 12 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   id                 43400 non-null  int64  
 1   gender             43400 non-null  object 
 2   age                43400 non-null  float64
 3   hypertension       43400 non-null  int64  
 4   heart_disease      43400 non-null  int64  
 5   ever_married       43400 non-null  object 
 6   work_type          43400 non-null  object 
 7   Residence_type     43400 non-null  object 
 8   avg_glucose_level  43400 non-null  float64
 9   bmi                41938 non-null  float64
 10  smoking_status     30108 non-null  object 
 11  stroke             43400 non-null  int64  
dtypes: float64(3), int64(4), object(5)
memory usage: 4.0+ MB

Come primo dato ci interessa sapere quanti uomini/donne/altro sono presenti nel dataset.

1.3 Quantità uomini e donne presenti nel dataset¶

In [4]:
px.histogram(df, x="gender", color="gender", 
             barmode="overlay", 
             title="Males/Females quantity", 
             height=500, 
             color_discrete_sequence=["blue", "red", "green"], 
             text_auto=True,
             labels={'gender': 'Gender'}).update_layout(yaxis_title="Count")

Ci sono solo 11 "Other" e, visto che sul totale il loro impatto sarebbe irrelevante e sarebbero scomodi da gestire come terza variabile per il genere, decidiamo di toglierli.

In [5]:
df = df.loc[df["gender"] != "Other"]

Capitolo 2: Comprensione e primo approccio ai dati¶

2.1 Distribuzione dell'eta¶

Diamo un'occhiama a come l'età è distribuita tra maschi e femmine, si può notare che, come visto in precedenza, il numero di donne è nettamente maggiore rispetto al numero di uomini. La maggior parte delle persone hanno un range di età compreso tra 20 e 60, poi c'è un crescendo intorno agli ottant'anni.

In [6]:
df.loc[df["stroke"] == 0, "stroke"] = "NotHad"
df.loc[df["stroke"] == 1, "stroke"] = "Had"

agesChart = px.histogram(df, x="age", 
                         color="gender", 
                         barmode="overlay", 
                         title="Age distribuition", 
                         marginal="box", 
                         height=500, 
                         color_discrete_sequence=["blue", "red"], 
                         opacity=.3,
                         labels={'age': 'Age', "gender": "Gender"}).update_layout(yaxis_title="Count")
agesChart.show()

strokeChart = px.histogram(df, x="age", 
                           color="stroke",
                           barmode="overlay", 
                           title="Age distribution of stroke", 
                           marginal="box", 
                           color_discrete_sequence=["green", "orange"],  
                           opacity=.8, 
                           height=500,
                           labels={'age': 'Age', "stroke": "Stroke"}).update_layout(yaxis_title="Count")
strokeChart.show()

C'é una donna (bimba) che ha avuto un ictus a 1.3 anni. Nonostante sia possibile avere un ictus così giovani decidiamo di mettere l'eta minima a 10 anni, dato che prima lo stile di vita non influisce sul fattore ictus. Salviamo comunque i dati delle persone con meno di 10 anni per un analisi futura.

In [7]:
dfUnder10 = df.loc[df["age"] < 10]
df = df.loc[df["age"] >= 10]
dfMales = df.loc[df["gender"] == "Male"]
dfFemales = df.loc[df["gender"] == "Female"]
dfSingle = df.loc[df["ever_married"] == "No"]
dfMarried = df.loc[df["ever_married"] == "Yes"]

2.2 Dati finali con cui si lavora¶

Abbiamo rimosso "Others" dalla colonna "Gender", e abbiamo rimosso le persone con età inferiore a 10 anni, poiché abbiamo notato che entrambi questi dati erano superflui. Di seguito sono presenti il totale dei dati che abbiamo tenuto, suddivisi per genere.

In [8]:
fig = go.Figure(go.Pie(
    values = [dfMales["gender"].count(), dfFemales["gender"].count()],
    labels = ["Males", "Females"],
    texttemplate = "%{label}: %{value} <br>(%{percent})",
    textposition = "inside",
    textfont=dict(
        family="sans serif",
        size=24,
        color="white"
    ),
   
))
fig.update_layout(
    width=600,
    height=600,
)
fig.update_layout(
    title={
        'text': "Males/Females percentages",
        'y':0.9,
        'x':0.5,
        'xanchor': 'center',
        'yanchor': 'top'})
fig.update_traces(marker=dict(colors=['blue', 'red']))
fig.show()

Capitolo 3: Analisi dei dati¶

3.1 Distribuizione degli ictus con il sesso e il matrimonio¶

Come prima cosa possiamo notare che il numero di persone sposate è nettamente maggiore rispetto al numero di persone single.

In [9]:
px.histogram(df, x="ever_married", color="ever_married", barmode="overlay", 
             title="Quantità uomini/donne", height=500, 
             color_discrete_sequence=["purple", "darkorange"], text_auto=True)

fig = go.Figure(go.Pie(
    values = [dfSingle["ever_married"].count(), dfMarried["ever_married"].count()],
    labels = ["Single", "Married"],
    texttemplate = "%{label}: %{value} <br>(%{percent})",
    textposition = "inside",
    textfont=dict(
        family="sans serif",
        size=24,
        color="white"
    )
))
fig.update_layout(
    width=600,
    height=600,
)
fig.update_layout(
    title={
        'text': "Married/Single percentages",
        'y':0.9,
        'x':0.5,
        'xanchor': 'center',
        'yanchor': 'top'})
colors =["purple", "darkorange"]
fig.update_traces(marker=dict(colors=colors))
fig.show()
In [10]:
grouped_df = df.groupby(["gender", "ever_married"])["stroke"].value_counts(normalize=True)*100
grouped_df = grouped_df.unstack()["Had"].reset_index()
grouped_df.loc[grouped_df["ever_married"] == "Yes", "ever_married"] = "Married"
grouped_df.loc[grouped_df["ever_married"] == "No", "ever_married"] = "Single"
grouped_df["name"] =  grouped_df["ever_married"] + " " + grouped_df['gender'].astype(str)

fig = px.histogram(grouped_df, y="name", x="Had", 
                   color="name", 
                   color_discrete_sequence=["red", "red", "blue", "blue"], 
                   title="Stroke rate by gender and marital status", 
                   labels={
                       "name": "", "Had" : ""
                   },
                   text_auto= ".3f",
                   height=500
                  )
fig.update_layout(showlegend=False)
fig.update_layout(yaxis_title="")
fig.update_layout(xaxis_title="Stroke percentage")
fig.show()

La conclusione che possiamo trarre guardando questo grafico, è che il matrimonio sembra aumentare il rischio di avere un ictus, ma il tutto è fatto con dei dati che rappresentano una maggioranza di persone sposate. Quindi non è improbabile che questa rappresentazione sia dovuta solo dal fatto che un dato sovrasta l'altro.

In [11]:
fig = px.box(df, x="age", 
             color="ever_married", 
             color_discrete_sequence=["purple", "darkorange"], 
             title="Age distribution by marital status", 
             labels={"ever_married": "Ever married", "age": "Age"},
             height=500
            )
fig.show()

Facciamo anche una distrubuzione dell'età in base allo stato coniugale. Si nota che le persone sposate (quindi quelle con più ictus) sono anche quelle mediamente più anziane. Decidiamo quindi di fare una media di ictus in base all'eta e lo stato coniugale per visualizzare meglio se il matrimonio può essere un fattore di rischio.

In [12]:
mydf = df.copy()
mydf.loc[mydf["stroke"] == "Had", "stroke"] = 1
mydf.loc[mydf["stroke"] == "NotHad", "stroke"] = 0


mydf = mydf.groupby(["age", "ever_married"])["stroke"].mean()
mydf = mydf.reset_index(name = "values")

fig = px.scatter(mydf, x = "age", 
                 y = "values", 
                 color="ever_married", 
                 trendline_options=dict(window=10),
                 title="Average number of strokes by age and marital status", 
                 trendline="rolling",
                 height=500, 
                 color_discrete_sequence=["purple", "darkorange"],
                 labels={"ever_married": "Ever married", "age": "Age"})
fig.update_yaxes(range=[-0.01, 0.11])
fig.update_xaxes(range=[20, 82])
fig.update_layout(yaxis_title="Avg(10) stroke percentage")
fig.data = [t for t in fig.data if t.mode == "lines"]
fig.update_traces(showlegend=True)
fig.show()

Questo ultimo grafico, mostra la media di ictus avuti in base all'età e allo stato coniugale. Si può notare chiaramente che il fattore che fa aumentare notevolmente il rischio di ictus è l'età in primis, e come secondo, a differenza di quello che potevamo notare prima, è che in realtà essere single aumenta anche se di poco, la possibilità di avere un ictus.

3.2 Probabilità di ictus in base alla storia da fumatore¶

Di seguito è presente un primo grafico in cui vengono divisi secondo il loro stato da fuamtore.

In [13]:
smokersHistogram = px.histogram(df, x="smoking_status", 
                                color="smoking_status", 
                                title = "Smokers count", 
                                color_discrete_sequence=["green", "red", "blue"], 
                                text_auto= True,
                                labels={"smoking_status": "Smoking status"},
                                height=500
                               )
smokersHistogram.update_layout(yaxis_title="Count")
smokersHistogram.show()

groupedSmokers = df.groupby(["smoking_status"])["stroke"].value_counts(normalize=True)*100
groupedSmokers = groupedSmokers.unstack()["Had"].reset_index()
fig = px.histogram(groupedSmokers, y="smoking_status", x="Had", 
                   color="smoking_status", 
                   title="Stroke rate according to smoking status", 
                   text_auto= ".3f",
                   labels={"smoking_status": "Smoking status"},
                   height=500
                  )
fig.update_layout(showlegend=False)
fig.update_layout(xaxis_title="Stroke percentage")
fig.update_layout(yaxis_title="")
fig.show()

Sembra che il se si fuma si ha la stessa probabilità di ictus di chi non ha mai fumato. E smettendo di fumare questa probabilità aumenta.

In [14]:
px.box(df, x="age", 
       color="smoking_status",
       title="Age distribution according to smoking status", 
       color_discrete_sequence=["green", "red", "blue"],
       labels={"smoking_status": "Smoking status", "age": "Age"},
       height=500
      )

Anche da questa analisi, come per il matrimonio si nota che le persone che hanno smesso di fumare sono quelle più anziane, ed é quindi logico che siano quelli con probabilità più alta

In [23]:
mydf = df.copy()
mydf.loc[mydf["stroke"] == "Had", "stroke"] = 1
mydf.loc[mydf["stroke"] == "NotHad", "stroke"] = 0


mydf = mydf.groupby(["age", "smoking_status"])["stroke"].mean()
mydf = mydf.reset_index(name = "values")

graph = px.scatter(mydf, x = "age", 
                   y = "values", 
                   color="smoking_status", 
                   trendline_options=dict(window=10),
                   title="Average number of strokes according to age and smoking status", 
                   trendline="rolling",
                   height=500,
                   labels={"smoking_status": "Smoking status", "age": "Age", "values": "Avg(10) stroke percentage"}
                  )
graph.data = [t for t in graph.data if t.mode == "lines"]
graph.update_traces(showlegend=True)
graph.update_yaxes(range=[-0.01, 0.1])
graph.update_xaxes(range=[20, 82])
graph.show()

Come si può notare in questo ultimo grafico, è evidente che, oltre all'età, il fumo è effettivamente un fattore a rischio. Soprattutto dai cinquanta anni in avanti c'è una crescita maggiore per chi fuma, e chi ha fumato, mentre chi non ha mai fumato rimane sempre inferiore agli altri due. Un altra cosa che si può notare è che dopo gli ottant'anni, le persone che non hanno mai fumato hanno una crescita maggiore rispetto le altre, e qua facciamo la supposizione che gran parte degli altri siano già deceduti e che quindi la media loro diminuisce.

3.3 BMI unito a glucosio¶

Come prima analisi guardiamo come sono correlati il BMI e il livello di glucosio con l'età che avanza.

In [16]:
bmiChart = px.box(df, x="age", 
                  y="bmi", 
                  title="Correlation of BMI with age", 
                  points=False, 
                  height=500,
                  labels={"bmi": "BMI", "age": "Age"}
                 )
bmiChart.update_yaxes(range=[17, 37])
bmiChart.show()

glucoseChart = px.box(df, x="age",
                      y="avg_glucose_level", 
                      title="Correlation of glucose level with age", 
                      points=False, 
                      height=500,
                      labels={"avg_glucose_level": "Avg. glucose", "age": "Age"}
                     )
glucoseChart.update_yaxes(range=[70, 190])
glucoseChart.show()

Notiamo subito che il BMI ha un crescendo intorno ai 50/60 anni per poi calare di nuovo. Come per il fumo, supponiamo che sia perché le persone con una massa maggiore, quindi vita poco sana, decedano prima. Notiamo che il livello di glucosio inizia a salire intorno ai 55 anni e continua a crescere.

Facciamo una mappa per mettere insieme il livello di glucosio e il BMI, per vedere come essi sono correlati con l'avere un ictus o meno.

In [17]:
df = df.sort_values(by=['age'])
bmiGlugoseScatter = px.scatter(df, x="avg_glucose_level", 
                               y="bmi", color="stroke" , 
                               opacity = .8, 
                               title="BMI map and glucose level correlated with stroke", 
                               height=500, 
                               color_discrete_sequence=["green", "orange"],
                               labels={"avg_glucose_level": "Avg. glucose", "bmi": "BMI", "stroke": "Stroke"}
                              )
bmiGlugoseScatter.show()

bmiGlugoseDensity = px.density_contour(df, x="avg_glucose_level",
                                       y="bmi", color="stroke",
                                       title="BMI and glucose level groups correlated with stroke",
                                       height=500, 
                                       color_discrete_sequence=["green", "orange"],
                                       labels={"avg_glucose_level": "Avg. glucose", "bmi": "BMI", "stroke": "Stroke"}
                                      )
bmiGlugoseDensity.update_xaxes(range=[40, 260])
bmiGlugoseDensity.update_yaxes(range=[11, 55])
bmiGlugoseDensity.show()

Dai grafici si puo notare che il bmi rimane stabile con l'età, ma i livelli di glucosio aumentano con l'aumentare degli anni. Nello scatter plot dove si mettono in relazione il bmi con i livelli di glucosio, si possono notare due gruppi di persone con l'ictus, uno sulla destra e uno sulla sinistra. Nell'ultimo grafico questi due gruppi si notano bene e si nota che il gruppo "destra", quindi quelli con i livelli di glucosio più alto sono completamente separati da quelli che non hanno avuto un ictus. Nel secondo grafico è rappresentata la mappa di densità, e si può notare che risaltano in maniera particolare due zone. Decidiamo quindi di fare una mappa, dividendo il BMI e i livelli di glucosio in gruppi per vedere quali sono quelli a rischio. Divisione fatta per il BMI:

  • 00.0 - 18.5 Sottopeso
  • 18.5 - 25.0 Peso norma
  • 25.0 - 30.0 Sovrappeso
  • 30.0+ Obesità

Divisione fatta per il livello di glucosio:

  • 0 - 70 Ipoglicemia
  • 70 - 125 Normale
  • 125 - 200 Alto
  • 200+ Molto alto
In [18]:
mydf =df[["bmi", "avg_glucose_level", "stroke"]]

mydf['BMIstatus'] = pd.cut(mydf['bmi'],
                      bins=[0, 18.5, 25, 30, float('Inf')],
                      labels=['Underweight', 'Standard weight', 'Overweight', 'Obesity'])
mydf['GlucoseStatus'] = pd.cut(mydf['avg_glucose_level'],
                      bins=[0, 70, 125, 200, float('Inf')],
                      labels=['Ipoglicemia', 'Normale', 'Alto', 'Molto alto'])

mydf = (mydf.groupby(["BMIstatus", "GlucoseStatus"])["stroke"].value_counts(normalize=True)*100).round(2)
mydf = mydf.unstack()["Had"].reset_index()

final = [ mydf["Had"].values[i * 4:(i + 1) * 5] for i in range((len( mydf["Had"].values) + 5 - 1) // 5 )]
fig = go.Figure(data=go.Heatmap(
                   z=final,
                   y=['Underweight', 'Standard weight', 'Overweight', 'Obesity'],
                   x=['Hypoglycaemia', 'Normal', 'High', 'Very high'],
                   hoverongaps = False, 
    texttemplate="%{z}\u0025"))
fig.update_layout(title = "Stroke rates based on bmi and glucose levels", height=500)
fig.show()

Dal grafico sovrastante si può notare in maniera chiara ed efficace quali valori tra bmi e livello di glucosio, causino un aumento della possibilità di avere un ictus.

3.4 ipertensione e problemi al cuore¶

Quello che abbiamo fatto è stato una semplice distribuizione dell'età, e abbiamo stabilito che per il tempo che ci è stato dato non è possibile approfondire un l'analisi guardando anche l'ipertensione e i problemi al cuore.

In [19]:
df.loc[df["hypertension"] == 0, "hypertension"] = "NotHad"
df.loc[df["hypertension"] == 1, "hypertension"] = "Had"

hypertensionChart = px.histogram(df, x="age", color="hypertension", barmode="overlay", 
                                 title="Age distribution of hypertension", 
                                 marginal="box",
                                 labels={"age": "Age", "hypertension": "Hypertension"},
                                 height = 500
                                )
hypertensionChart.update_layout(yaxis_title="Count")
hypertensionChart.show()

df.loc[df["heart_disease"] == 0, "heart_disease"] = "NotHad"
df.loc[df["heart_disease"] == 1, "heart_disease"] = "Had"

heartChart = px.histogram(df, x="age", 
                          color="heart_disease", 
                          barmode="overlay", 
                          title="Age distribution of heart disease", 
                          marginal="box",
                          labels={"age": "Age", "heart_disease": "Heart disease"},
                          height = 500
                         )
heartChart.update_layout(yaxis_title="Count")
heartChart.show()

3.5 lavoro¶

In [20]:
workHistogram = px.histogram(df, x="work_type", 
                             color="stroke",
                             barmode="group", 
                             title="Type of stroke-related work", 
                             color_discrete_sequence=["green", "orange"],
                             labels={"stroke": "Stroke", "work_type": "Work type"},
                             height=500
                            )

workHistogram.update_layout(yaxis_title="Count")
workHistogram.show()

Le persone sembrano preferire il lavoro in aziende private, mentre il numero di lavoratori autonomi/lavori pubblici sembra essere simile (i bambini possono essere ignorati). Mentre i disoccupati sono molto pochi.

I dipendenti privati sembrano soffrire di ictus più di altri tipi di lavoro (forse a causa della pressione lavorativa).

In [21]:
mydf = df.copy()
mydf = mydf.filter(items=["work_type", "stroke"])
mydf["hadStroke"] = mydf["stroke"] == "Had"
mydf = mydf.groupby(["work_type"])["hadStroke"].mean()
mydf = mydf.reset_index(name = "mean")
fig  = px.histogram(mydf,  
                    x="work_type",  
                    y="mean", 
                    title="Stroke rate by type of work",
                    labels={'count':'', "work_type": "Work type"},
                    height=500
                   )
fig.update_traces(text= [f'{val:.3f}\u0025' for val in mydf['mean']])
fig.update_yaxes(visible=False, showticklabels=False)


fig.show()

Normalizzando i valori si nota però che invece sono le persone con un lavoro indipendente ad avere il valore di ictus più alto. Mentre i dipendenti privati e i dipendenti governativi, hanno un valore molto simile.

Capitolo 4: Conclusione¶

Per concludere questo percorso, si può affermare che alcuni fattori scatenanti per avere un ictus possono essere:

  • fumare
  • essere single
  • livelli di glucosio e del BMI al di fuori della norma
  • l'avanzare dell'età

Quello che è stato evidente, è che tra questi fattori l'avanzare dell'età è il più scatenante.